Long only 1/n portfolio#
import pandas as pd
pd.options.plotting.backend = "plotly"
import yfinance as yf
from cvx.simulator.builder import builder
from cvx.simulator.grid import resample_index
data = yf.download(tickers = "SPY AAPL GOOG MSFT", # list of tickers
period = "10y", # time period
interval = "1d", # trading interval
prepost = False, # download pre/post market hours data?
repair = True) # repair obvious price errors e.g. 100x?
[ 0% ]
[**********************50% ] 2 of 4 completed
[**********************75%*********** ] 3 of 4 completed
[*********************100%***********************] 4 of 4 completed
1 Failed download:
['SPY']: Exception("The following 'Dividends' events are out-of-range, did not expect with interval 1d: DatetimeIndex(['2013-06-21 00:00:00-04:00', '2013-09-20 00:00:00-04:00',\n '2013-12-20 00:00:00-05:00', '2014-03-21 00:00:00-04:00',\n '2014-06-20 00:00:00-04:00', '2014-09-19 00:00:00-04:00',\n '2014-12-19 00:00:00-05:00', '2015-03-20 00:00:00-04:00',\n '2015-06-19 00:00:00-04:00', '2015-09-18 00:00:00-04:00',\n '2015-12-18 00:00:00-05:00', '2016-03-18 00:00:00-04:00',\n '2016-06-17 00:00:00-04:00', '2016-09-16 00:00:00-04:00',\n '2016-12-16 00:00:00-05:00', '2017-03-17 00:00:00-04:00',\n '2017-06-16 00:00:00-04:00', '2017-09-15 00:00:00-04:00',\n '2017-12-15 00:00:00-05:00', '2018-03-16 00:00:00-04:00',\n '2018-06-15 00:00:00-04:00', '2018-09-21 00:00:00-04:00',\n '2018-12-21 00:00:00-05:00', '2019-03-15 00:00:00-04:00',\n '2019-06-21 00:00:00-04:00', '2019-09-20 00:00:00-04:00',\n '2019-12-20 00:00:00-05:00', '2020-03-20 00:00:00-04:00',\n '2020-06-19 00:00:00-04:00', '2020-09-18 00:00:00-04:00',\n '2020-12-18 00:00:00-05:00', '2021-03-19 00:00:00-04:00',\n '2021-06-18 00:00:00-04:00', '2021-09-17 00:00:00-04:00',\n '2021-12-17 00:00:00-05:00', '2022-03-18 00:00:00-04:00',\n '2022-06-17 00:00:00-04:00', '2022-09-16 00:00:00-04:00',\n '2022-12-16 00:00:00-05:00', '2023-03-17 00:00:00-04:00',\n '2023-06-16 00:00:00-04:00'],\n dtype='datetime64[ns, America/New_York]', freq=None)")
prices = data["Adj Close"]
capital = 1e6
b = builder(prices=prices, initial_cash=capital)
for time, state in b:
# each day we invest a quarter of the capital in the assets
b[time[-1]] = 0.25 * state.nav / state.prices
portfolio = b.build()
portfolio.profit.cumsum().plot()
portfolio.nav.plot()
Rebalancing#
Usually we would not execute on a daily basis but rather rebalance every week, month or quarter. There are two approaches to deal with this problem in cvxsimulator.
Resample the existing daily portfolio (helpful to see effect of your hesitated trading)
Trade only on days that are within a predefined grid (most flexible if you have a rather irregular grid)
Resample an existing portfolio#
portfolio_resampled = portfolio.resample(rule="M")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 1
----> 1 portfolio_resampled = portfolio.resample(rule="M")
File ~/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/cvx/simulator/portfolio.py:430, in EquityPortfolio.resample(self, rule)
420 """The resample method resamples an EquityPortfolio object to a new frequency
421 specified by the rule argument.
422 A new EquityPortfolio object is created with the original prices
(...)
427 but rather returns a new object.
428 """
429 # iron out the stocks index
--> 430 stocks = iron_frame(self.stocks, rule=rule)
432 return EquityPortfolio(
433 prices=self.prices,
434 stocks=stocks,
435 trading_cost_model=self.trading_cost_model,
436 initial_cash=self.initial_cash,
437 )
File ~/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/cvx/simulator/grid.py:17, in iron_frame(frame, rule)
8 def iron_frame(frame, rule):
9 """
10 The iron_frame function takes a pandas DataFrame
11 and keeps it constant on a coarser grid.
(...)
15 :return: the ironed frame
16 """
---> 17 s_index = resample_index(frame.index, rule)
18 return _project_frame_to_grid(frame, s_index)
File ~/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/cvx/simulator/grid.py:31, in resample_index(index, rule)
22 """
23 The resample_index function resamples a pandas DatetimeIndex object
24 to a lower frequency using a specified rule.
(...)
28 but rather returns a pandas DatetimeIndex
29 """
30 series = pd.Series(index=index, data=index)
---> 31 a = series.resample(rule=rule).first()
32 return pd.DatetimeIndex(a.values)
File ~/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/pandas/core/series.py:5719, in Series.resample(self, rule, axis, closed, label, convention, kind, on, level, origin, offset, group_keys)
5704 @doc(NDFrame.resample, **_shared_doc_kwargs) # type: ignore[has-type]
5705 def resample(
5706 self,
(...)
5717 group_keys: bool = False,
5718 ) -> Resampler:
-> 5719 return super().resample(
5720 rule=rule,
5721 axis=axis,
5722 closed=closed,
5723 label=label,
5724 convention=convention,
5725 kind=kind,
5726 on=on,
5727 level=level,
5728 origin=origin,
5729 offset=offset,
5730 group_keys=group_keys,
5731 )
File ~/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/pandas/core/generic.py:8888, in NDFrame.resample(self, rule, axis, closed, label, convention, kind, on, level, origin, offset, group_keys)
8885 from pandas.core.resample import get_resampler
8887 axis = self._get_axis_number(axis)
-> 8888 return get_resampler(
8889 cast("Series | DataFrame", self),
8890 freq=rule,
8891 label=label,
8892 closed=closed,
8893 axis=axis,
8894 kind=kind,
8895 convention=convention,
8896 key=on,
8897 level=level,
8898 origin=origin,
8899 offset=offset,
8900 group_keys=group_keys,
8901 )
File ~/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/pandas/core/resample.py:1523, in get_resampler(obj, kind, **kwds)
1519 """
1520 Create a TimeGrouper and return our resampler.
1521 """
1522 tg = TimeGrouper(**kwds)
-> 1523 return tg._get_resampler(obj, kind=kind)
File ~/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/pandas/core/resample.py:1713, in TimeGrouper._get_resampler(self, obj, kind)
1704 elif isinstance(ax, TimedeltaIndex):
1705 return TimedeltaIndexResampler(
1706 obj,
1707 timegrouper=self,
(...)
1710 gpr_index=ax,
1711 )
-> 1713 raise TypeError(
1714 "Only valid with DatetimeIndex, "
1715 "TimedeltaIndex or PeriodIndex, "
1716 f"but got an instance of '{type(ax).__name__}'"
1717 )
TypeError: Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex, but got an instance of 'Index'
frame = pd.DataFrame({"original": portfolio.nav, "monthly": portfolio_resampled.nav})
frame
| original | monthly | |
|---|---|---|
| Date | ||
| 2013-05-30 | 1.000000e+06 | 1.000000e+06 |
| 2013-05-31 | 9.945921e+05 | 9.945921e+05 |
| 2013-06-03 | 1.000400e+06 | 1.000391e+06 |
| 2013-06-04 | 9.917402e+05 | 9.917309e+05 |
| 2013-06-05 | 9.846364e+05 | 9.846127e+05 |
| ... | ... | ... |
| 2023-05-23 | 7.505451e+06 | 7.493060e+06 |
| 2023-05-24 | 7.461416e+06 | 7.447306e+06 |
| 2023-05-25 | 7.603349e+06 | 7.590982e+06 |
| 2023-05-26 | 7.711937e+06 | 7.698989e+06 |
| 2023-05-30 | 7.735463e+06 | 7.721169e+06 |
2518 rows × 2 columns
print(portfolio_resampled.stocks)
AAPL GOOG MSFT SPY
Date
2013-05-30 17854.390520 11527.266005 8564.268967 1814.033248
2013-05-31 17854.390520 11527.266005 8564.268967 1814.033248
2013-06-03 17895.614489 11573.477982 8432.882983 1831.100869
2013-06-04 17895.614489 11573.477982 8432.882983 1831.100869
2013-06-05 17895.614489 11573.477982 8432.882983 1831.100869
... ... ... ... ...
2023-05-23 10590.586692 16651.905094 5882.633509 4316.566726
2023-05-24 10590.586692 16651.905094 5882.633509 4316.566726
2023-05-25 10590.586692 16651.905094 5882.633509 4316.566726
2023-05-26 10590.586692 16651.905094 5882.633509 4316.566726
2023-05-30 10590.586692 16651.905094 5882.633509 4316.566726
[2518 rows x 4 columns]
# almost hard to see that difference between the original and resampled portfolio
frame.plot()
# number of shares traded
portfolio_resampled.trades_stocks.iloc[1:].plot()
Trade only days in predefined grid#
b = builder(prices=prices, initial_cash=capital)
# define a grid
grid = resample_index(prices.index, rule="M")
for time, state in b:
# each day we invest a quarter of the capital in the assets
if time[-1] in grid:
b[time[-1]] = 0.25 * state.nav / state.prices
else:
# forward fill an existing position
b[time[-1]] = b[time[-2]]
portfolio = b.build()
portfolio.nav.plot()
# Trading only once a month can lead to days where 150k had to be reallocated
portfolio.turnover.iloc[1:].plot()
Why not resampling the prices?#
I don’t believe in bringing the prices to a monthly grid. This would render it hard to construct signals given the sparse grid. We stay on a daily grid and trade once a month.